Java虚拟机是基于栈结构的,如下:

栈帧的概念

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧中存储了方法的局部变量表,操作数栈,动态链接和方法返回地址的信息。每一个方法从调用到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

  • 局部变量表

    局部变量表示一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(variable slot)为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型。

    在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

    虚拟机通过索引定位的方法查找对应的局部变量,索引的范围是从0~局部变量表最大容量,如果Slot是32位的,则遇到一个64位数据类型的变量,则会连续使用两个连续的slot来存储。

  • 操作数栈

    它是一个后入先出栈,同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的max_stacks数据项中。

    操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。

    当一个方法刚刚开始执行时,其操作数栈是空的,随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈中,再随着计算的进行将栈中元素出栈道局部变量表或返回给方法调用者,也就是出栈/入栈的过程。

一个字节码指令以及操作数出栈/入栈的例子

问题:这个程序运行后会输出哪三个数字(以及test1和test2函数中return和finally的执行情况)

System.out.println(test1(num)) —- 60
System.out.println(b); —- 60
System.out.println(test2(num)); —–30
执行情况,且看下面讲解

学Java时我们都知道:

  • 执行完try中的语句后,无论是否有异常被catch到,finally中的语句都会被执行(除了exit以及其它异常外),所以finally中通常用于关闭流关闭连接等操作。

  • finally中如果有return语句,则会用finally中的语句覆盖掉try/catch中的return。

1
2
3
4
5
6
7
8
9
public static int test1(int a){
try{
a+=20;
return a;
} finally {
a+=30;
return a;
}
}

于是,在test1中,try块中return时a的值为30,经过finally块+30后,值变为60,再return就是返回了finally中的a,即60。于是第一个输出为60,这个很简单。

1
2
3
4
5
6
7
8
9
public static int test2(int b){
try{
b+=20;
return b;
}finally {
b+=30;
System.out.println(b);
}
}

在test2中,try块中经过计算后return的b值为30,finally中没有返回语句,故return的b值以try中的b=30为准(即第三个输出为30)。

test2()方法字节码中的各个属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public static int test2(int);
descriptor: (I)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iinc 0, 20
3: iload_0
4: istore_1
5: iinc 0, 30
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iload_0
12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
15: iload_1
16: ireturn
17: astore_2
18: iinc 0, 30
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: iload_0
25: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
28: aload_2
29: athrow
Exception table:
from to target type
0 5 17 any
LineNumberTable:
line 17: 0
line 18: 3
line 20: 5
line 21: 8
line 18: 15
line 20: 17
line 21: 21
line 22: 28
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 b I
StackMapTable: number_of_entries = 1
frame_type = 81 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]

方法描述符descriptor为I

test2()方法的访问标志为:ACC_PUBLIC和ACC_STATIC表示此方法的修饰符有public和static

属性表attribute_info中的“Code”属性:即为属性表集合,包括了:代码转换后字节码指令+Exceptiontable+LineNumberTable+LocalVariableTable+StackMapTable

Exceptiontable是异常表用于处理异常后的程序出口。

LineNumberTable:行号表,用于指示Java源码行号和字节码指令的对应关系

LocalVariableTable:局部变量表,用于存放运行期间和操作数栈交互(出栈/入栈)的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Code:
stack=2, locals=3, args_size=1 局部变量表 操作数栈
0: iinc 0, 20 //自增指令,位于局部变量表中0号位置的int型数值+20 30 null
3: iload_0 //从局部变量表0号位置加载一个int型值到操作数栈 30 30
4: istore_1 //从操作数栈顶出栈一个int型值存到局部变量表1号位置 30,30 null
5: iinc 0, 30 //局部变量表中0号位置的int型数值加30 60,30 null
8: getstatic #2 //访问类的静态字段值。#2表示静态字段位于运行时 60,30 null
11: iload_0 //从局部变量表0号位置加载一个int型值到操作数栈 60,30 60
12: invokevirtual #3 //调用PrintStream类的实例方法——println输出60 60,30 null
15: iload_1 //从局部变量表1号位置加载一个int型值到操作数栈 60,30 30
16: ireturn //返回一个int型数值(从栈顶) 60, 30 null
17: astore_2
18: iinc 0, 30
21: getstatic #2
24: iload_0
25: invokevirtual #3
28: aload_2
29: athrow //抛出异常,程序跳转到异常处理器中,(Exception table)
Exception table:
from to target type
0 5 17 any

上述代码中序号为0的指令inc对应test2源码中try块中:b += 20,此处test2()方法是在主函数main()中被调用的,在main()方法栈帧中操作数出栈一个int型值10,作为test2()方法调用的参数,test2()方法调用时,会新构建test2()方法的栈帧(从而成为当前栈),10作为参数就存到了当前栈帧的局部变量表0号位置,所以在执行0: iinc 0, 20时,test2()方法栈帧中局部变量表0号位置已经有了10这个值。

有几点需要注意:

  • getstatic
  • invokevirtual
  • ireturn

getstatic指令用于访问类的静态字段值

以第一个getstatic指令为例,其后的参数#2,表示会在FinallyTest类的运行时常量池中2号位置查找此字段值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Constant pool:
#1 = Methodref #7.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #31.#32 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #33.#34 // java/io/PrintStream.println:(I)V
#4 = Methodref #6.#35 // JustCoding/Practise/FinallyTest.test1:(I)I
#5 = Methodref #6.#36 // JustCoding/Practise/FinallyTest.test2:(I)I
#6 = Class #37 // JustCoding/Practise/FinallyTest
#7 = Class #38 // java/lang/Object
.....
#30 = NameAndType #8:#9 // "<init>":()V
#31 = Class #40 // java/lang/System
#32 = NameAndType #41:#42 // out:Ljava/io/PrintStream;
#33 = Class #43 // java/io/PrintStream
#34 = NameAndType #44:#45 // println:(I)V
#35 = NameAndType #15:#16 // test1:(I)I
#36 = NameAndType #21:#16 // test2:(I)I
.....

常量池中2号位置的字段值是符号引用,该引用指向的是常量池中31号和32号位置,即java/lang/System类和java/io/PrintStream类中println的方法描述。如果该静态字段(#2号)所指向的类或接口没有被初始化,则指令执行过程将触发其初始化过程。

invokevirtual指令用于调用实例方法

此处调用java.io.PrintStream类中的println方法后,会自动从test2的操作数栈中出栈相应的参数(即60)。然后方法执行来到了println方法中,于是新建此方法的栈帧,将当前栈帧切换到println栈帧上,将参数60入栈,在完成println方法后(输出60)再切换回到test2的栈帧中。

所以,第二个System.out.println(b);输出的是60.

iteturn指令用于从操作数栈顶出栈一个int型的值给方法调用者

说明

转自Java虚拟机-栈帧、操作数栈和局部变量表